上次分析Android O广播的问题遗留了一个东西没提,那就是官方推荐使用的JobScheduler
。这篇就简单了解一下这是个什么东西。
JobScheduler是什么
JobScheduler
允许开发者创建在后台执行的job,当预置的条件被满足时,这些Job将会在后台被执行。
在Android开发中,我们会遇到很多这样的情况,比如在未来的某个时间点或者未来满足某种条件(比如插入电源或者连接WiFi)的情况下下去执行一些操作。在Android L上,Google提供了一个叫做JobScheduler
的组件来帮助我们处理这种情况。
JobScheduler
Api可以在我们的App中执行一些操作,这些操作将会在我们预置的一些条件被满足的时候被执行。和AlarmManager
不一样,执行这些操作的时间并不是严格准确的。 JobScheduler
会把一系列的job收集起来一起执行,这样既允许我们的job被执行,又能兼顾到手机电量的使用情况,达到节电的目的。
JobScheduler怎么用
JobScheduler的使用非常简单,只需要三步:
- 创建JobService类
- 创建JobInfo,通过builder设定Job的执行选项
- 获取JobScheduler服务执行任务
下面按照这三步放一个简单代码示例。
JobService
JobService
的作用是,在JobScheduler
监测到系统状态达到对应启动条件时,会启动JobService
执行任务。所以我们需要继承JobService
创建一个自己的service,然后实现onStartJob
和onStopJob
这两个方法。
public class JobSchedulerService extends JobService {
private static final int MESSAGE_ID = 100;
private Handler mJobHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Log.i("WOW", "handle message!!!!");
//请注意,我们手动调用了jobFinished方法。
//当onStartJob返回true的时候,我们必须在合适时机手动调用jobFinished方法
//否则该应用中的其他job将不会被执行
jobFinished((JobParameters) msg.obj, false);
//第一个参数JobParameter来自于onStartJob(JobParameters params)中的params,
// 这也说明了如果我们想要在onStartJob中执行异步操作,必须要保存下来这个JobParameter。
return true;
}
});
// JobService运行在主线程 需要另外开启线程做耗时工作
@Override
public boolean onStartJob(JobParameters params) {
Log.i("WOW", "onStartJob");
// 注意到我们在使用Hanlder的时候把传进来的JobParameters保存下来了
mJobHandler.sendMessage(Message.obtain(mJobHandler, MESSAGE_ID, params));
// 返回false说明job已经完成 不是个耗时的任务
// 返回true说明job在异步执行 需要手动调用jobFinished告诉系统job完成
// 这里我们返回了true,因为我们要做耗时操作。
// 返回true意味着耗时操作花费的事件比onStartJob执行的事件更长
// 并且意味着我们会手动的调用jobFinished方法
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.i("WOW", "onStopJob");
mJobHandler.removeMessages(MESSAGE_ID);
// 当系统收到一个cancel job的请求时,并且这个job仍然在执行(onStartJob返回true),系统就会调用onStopJob方法。
// 但不管是否调用onStopJob,系统只要收到取消请求,都会取消该job
// true 需要重试
// false 不再重试 丢弃job
return false;
}
然后在Manifest文件给service添加一个权限
JobInfo
JobInfo
是对任务的描述,比如说需要监听哪些状态、重试策略、任务执行时间、是否持久化等等。 JobInfo.Builder
的构造函数需要传入一个jobId
,是Job
的唯一标志,后续通过该jobId
来取消Job
。 通过Builder
模式构造JobInfo
。
//Builder构造方法接收两个参数,第一个参数是jobId,每个app或者说uid下不同的Job,它的jobId必须是不同的
//第二个参数是我们自定义的JobService,系统会回调我们自定义的JobService中的onStartJob和onStopJob方法
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID,
new ComponentName(this, JobSchedulerService.class));
builder.setMinimumLatency(2000) // 2s后执行
.setOverrideDeadline(10000); // 最晚10s后执行
JobScheduler
通过服务获取JobScheduler
执行任务即可。
JobScheduler mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
int result = mJobScheduler.schedule(builder.build());
if (result <= 0) {
Log.i("WOW", "result is " + result + " Schedule failed");
}
JobScheduler API详解
JobService细节
在前面的代码注释已经有所说明
启动任务之后,会调用onStartJob方法,因为JobService运行在主线程,所以如果在任务开始时,如果要执行耗时的操作,就需要创建一个线程去做。
如果onStartJob执行的是不耗时的任务,就可以返回false,表示任务执行结束。
如果onStartJob起了一个线程执行耗时任务,就要返回true,表示任务还在执行,需要等任务真正结束后手动调用JobFinished()方法告诉系统任务已经结束。
JobInfo细节
JobInfo job=new JobInfo.Builder(i,componentName)
.setMinimumLatency(5000)//最小延时 5秒
.setOverrideDeadline(60000)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意网络
.setMinimumLatency(5000)//5秒 最小延时、
.setOverrideDeadline(60000)//maximum最多执行时间
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)//免费的网络---wifi 蓝牙 USB
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意网络---
/**
设置重试/退避策略,当一个任务调度失败的时候执行什么样的测量采取重试。
initialBackoffMillis:第一次尝试重试的等待时间间隔ms
*backoffPolicy:对应的退避策略。比如等待的间隔呈指数增长。
*/
.setBackoffCriteria(long initialBackoffMillis, int backoffPolicy)
.setBackoffCriteria(JobInfo.MAX_BACKOFF_DELAY_MILLIS, JobInfo.BACKOFF_POLICY_LINEAR)
.setPeriodic (long intervalMillis)//设置执行周期,每隔一段时间间隔任务最多可以执行一次。
.setPeriodic(long intervalMillis,long flexMillis)//在周期执行的末端有一个flexMiliis长度的窗口期,任务就可以在这个窗口期执行。
//设置设备重启后,这个任务是否还要保留。需要权限: RECEIVE_BOOT_COMPLETED
.setPersisted(boolean isPersisted);
.setRequiresCharging(boolean )//是否需要充电
.setRequiresDeviceIdle(boolean)//是否需要等设备出于空闲状态的时候
.addTriggerContentUri(uri)//监听uri对应的数据发生改变,就会触发任务的执行。
.setTriggerContentMaxDelay(long duration)//设置Content发生变化一直到任务被执行中间的最大延迟时间
//设置Content发生变化一直到任务被执行中间的延迟。如果在这个延迟时间内content发生了改变,延迟时间会重写计算。
.setTriggerContentUpdateDelay(long durationMilimms)
需要注意的是
setRequiredNetworkType(int networkType)
,setRequiresCharging(boolean requireCharging)
,setRequiresDeviceIdle(boolean requireIdle)
这几个方法可能会使得你的任务无法执行,除非调用setOverrideDeadline(long time)设置了最大延迟时间,使得你的任务在为满足条件的情况下也会被执行。
setMinimumLatency(long minLatencyMillis)
:这个方法指定我们的Job至少要多少毫秒之后执行,比如setMinimumLatency(5000)
,就表明我们这是了这个JobScheduler之后,这个Job至少要5秒之后执行,前五秒肯定是不会执行的。这个参数和setPeriodic
互斥。两个同时设置会抛出异常。
setRequiredNetworkType(int networkType)
:来启动我们这个Job时所需要的网络类型,一共有三个值JobInfo.NETWORK_TYPE_NONE
表明启动我们这个Job时不需要任何的网络连接;JobInfo.NETWORK_TYPE_ANY
表明启动我们这个Job时只要连着网就可以,不要求网络类型。JobInfo.NETWORK_TYPE_UNMETERED
表明启动我们这个Job时需要连接Wifi.
Android O 对JobScheduler的改进
您现在可以将工作队列与计划作业关联。要将一个工作项添加到作业的队列中,请调用
JobScheduler.enqueue()
)。当作业运行时,它可以将待定工作从队列中剥离并进行处理。这种功能可以处理之前需要启动后台服务(尤其是实现IntentService
的服务)的许多用例。您现在可以通过调用
JobInfo.Builder.setClipData()
) 的方式将ClipData
与作业关联。利用此选项,您可以将 URI 权限授予与作业关联,类似于这些权限传递到Context.startService()
的方式。您也可以将 URI 权限授予用于工作队列上的 intent。计划作业现在支持多个新的约束条件:
JobInfo.isRequireStorageNotLow()
)如果设备的可用存储空间非常低,作业将不会运行。
JobInfo.isRequireBatteryNotLow()
)如果电池电量等于或低于临界阈值,作业将不会运行;临界阈值是指设备显示 Low battery warning 系统对话框的电量。
-
作业需要一个按流量计费的网络连接,比如大多数移动数据网络数据套餐。
源码分析
JobSchedulerService启动
首先因为知道JobScheduler是通过系统服务拿到的:
(JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
所以可以想到,Android启动时所有的系统服务都是在SystemServer里启动:
//frameworks/base/services/java/com/android/server/SystemServer.java
mSystemServiceManager.startService(JobSchedulerService.class);
于是代码进入JobSchedulerService.java
//frameworks/base/services/core/java/com/android/server/job/JobSchedulerService.java
public final class JobSchedulerService extends com.android.server.SystemService
implements StateChangedListener, JobCompletedListener {
官方代码注释里有一句说明:
The JobSchedulerService knows nothing about constraints, or the state of active jobs. It receives callbacks from the various controllers and completed jobs and operates accordingly.
就是JobSchedulerService
对Job的状态和约束都不了解,完全是通过各种controller的回调去处理各种Job。
然后我们看其构造函数
public JobSchedulerService(Context context) {
super(context);
// 先创建在主线程的JobHandler
mHandler = new JobHandler(context.getMainLooper());
mConstants = new Constants(mHandler);
// binder服务端
mJobSchedulerStub = new JobSchedulerStub();
mJobs = JobStore.initAndGet(this);
// Create the controllers.
mControllers = new ArrayList();
mControllers.add(ConnectivityController.get(this));
mControllers.add(TimeController.get(this));
mControllers.add(IdleController.get(this));
mControllers.add(BatteryController.get(this));
mControllers.add(AppIdleController.get(this));
mControllers.add(ContentObserverController.get(this));
mControllers.add(DeviceIdleJobsController.get(this));
}
JobHandler
是一个在主线程运行的Handler,主要处理四个消息。
private class JobHandler extends Handler {
public JobHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message message) {
synchronized (mLock) {
if (!mReadyToRock) {
return;
}
}
switch (message.what) {
case MSG_JOB_EXPIRED:
...
break;
case MSG_CHECK_JOB:
...
break;
case MSG_CHECK_JOB_GREEDY:
...
break;
case MSG_STOP_JOB:
...
break;
}
maybeRunPendingJobsH();
// Don't remove JOB_EXPIRED in case one came along while processing the queue.
removeMessages(MSG_CHECK_JOB);
}
...
}
Constants
这里面定义了一些与系统全局设置保持同步的常量。 这一类或其任何域的访问应该同时持有JobSchedulerService.mLock锁来完成。
private final class Constants extends ContentObserver {}
JobSchedulerStub
JobSchedulerStub作为实现接口IJobScheduler的binder服务端。
final class JobSchedulerStub extends IJobScheduler.Stub {}
JobStore.initAndGet
这个方法是创建了一个JobStore的单例由JobSchedulerService使用。
// frameworks/base/services/core/java/com/android/server/job/JobStore.java
/** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
static JobStore initAndGet(JobSchedulerService jobManagerService) {
synchronized (sSingletonLock) {
if (sSingleton == null) {
sSingleton = new JobStore(jobManagerService.getContext(),
jobManagerService.getLock(), Environment.getDataDirectory());
}
return sSingleton;
}
}
JobStore
该类的作用是维护作业计划程序正在跟踪的作业主列表。 这些作业通过引用进行比较,因此此类中的任何函数都不应复制。 还处理持久作业的读/写。
创建一个JobStore实例,进行从磁盘读取文件。该方法会创建job目录以及jobs.xml文件, 以及从文件中读取所有的JobStatus。
private JobStore(Context context, Object lock, File dataDir) {
mLock = lock;
mContext = context;
mDirtyOperations = 0;
File systemDir = new File(dataDir, "system");
File jobDir = new File(systemDir, "job");
jobDir.mkdirs();
// 创建/data/system/job/jobs.xml
mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
mJobSet = new JobSet();
readJobMapFromDisk(mJobSet);
}
readJobMapFromDisk方法从磁盘读取Job信息,先看一下Job.xml文件结构
可以看出这个xml中主要记录了每一个Job的jobid, JobService的名字,包名,以及触发该Job的一些条件信息。
对于xml解析就不分析了,这个思路都一样的,就是流程上是从xml中读取job的信息,然后利用这些信息创建JobStatus
, JobStatus
对象包含了JobInfo信息(Jobid,package,class),还有该Job的delay,deadline信息,用于schedule。JobStatus添加到mJobSet。
StateController
构造函数还创建了7个StateController:
类型 | 说明 |
---|---|
ConnectivityController | 注册监听网络连接状态的广播 |
TimeController | 注册监听job时间到期的广播 |
IdleController | 注册监听屏幕亮/灭,dream进入/退出,状态改变的广播 |
BatteryController | 注册监听电池是否充电,电量状态的广播 |
AppIdleController | 监听app是否空闲 |
ContentObserverController | 通过ContentObserver监测content URIs的变化 |
DeviceIdleJobsController | 根据doze状态为app设置约束。 |
前面提到过,JobSchedulerService
是根据这些controller的回调处理Job的,所以简单看一下ConnectivityController
public interface StateChangedListener {
public void onControllerStateChanged();
public void onRunJobNow(JobStatus jobStatus);
public void onDeviceIdleStateChanged(boolean deviceIdle);
}
public abstract class StateController {
protected static final boolean DEBUG = JobSchedulerService.DEBUG;
protected final Context mContext;
protected final Object mLock;
protected final StateChangedListener mStateChangedListener;
public StateController(StateChangedListener stateChangedListener, Context context,
Object lock) {
mStateChangedListener = stateChangedListener;
mContext = context;
mLock = lock;
}
public abstract void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob);
public void prepareForExecutionLocked(JobStatus jobStatus) {}
public abstract void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, boolean forUpdate);
public void rescheduleForFailure(JobStatus newJob, JobStatus failureToReschedule) {}
public abstract void dumpControllerStateLocked(PrintWriter pw, int filterUid);
}
public class ConnectivityController extends StateController implements ConnectivityManager.OnNetworkActiveListener {
public static ConnectivityController get(JobSchedulerService jms) {
synchronized (sCreationLock) {
if (mSingleton == null) {
//单例模式
mSingleton = new ConnectivityController(jms, jms.getContext());
}
return mSingleton;
}
}
private ConnectivityController(StateChangedListener stateChangedListener, Context context) {
super(stateChangedListener, context);
//注册监听网络连接状态的广播,且采用BackgroundThread线程
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
mContext.registerReceiverAsUser(
mConnectivityChangedReceiver, UserHandle.ALL, intentFilter, null,
BackgroundThread.getHandler());
ConnectivityService cs =
(ConnectivityService)ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
if (cs != null) {
if (cs.getActiveNetworkInfo() != null) {
mNetworkConnected = cs.getActiveNetworkInfo().isConnected();
}
mNetworkUnmetered = mNetworkConnected && !cs.isActiveNetworkMetered();
}
}
}
所以可以知道,Android O以后禁止了一些广播的发送后,都是由这些Controller进行动态注册广播,由这些controller转交给JobScheduler进行处理。
流程控制
对遗留问题的说明
所以很明显,Android Framework对JobInfo已经设计好一些状态处理,比如说网络变化。所以这样不再用广播吊起更多App而引起性能问题了。
Ref